/*
 * Copyright 2006 Matt Jensen
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations under the License.
 */
package org.spectaql.internal.proxy.cglib;

import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import org.spectaql.internal.proxy.ProxyInternal;
import org.spectaql.internal.util.CollectionUtils;
import org.spectaql.internal.util.Extractor;
import org.spectaql.metadata.EntityMetadata;
import org.spectaql.metadata.PropertyMetadata;

import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Map;

/**
 * <p>
 * <code>PropertyAccessorCallback</code> is an implementation of the CGLib {@link MethodInterceptor} interface which
 * handles invocations of entity property accessor methods.
 * </p>
 * <p>
 * <strong>Thread Safety:</strong> Instances of this class are thread safe.
 * </p>
 */
class PropertyAccessorCallback implements MethodInterceptor
{
    /**
     * <p>
     * The entity metadata.
     * </p>
     */
    private final EntityMetadata m_entityMetadata;

    /**
     * <p>
     * Map of read (getter) methods to the corresponding {@link PropertyMetadata} instances.
     * </p>
     */
    private final Map<Method, PropertyMetadata> m_readMethodPropertyMetadata;

    /**
     * <p>
     * Map of write (setter) methods to the corresponding {@link PropertyMetadata} instances.
     * </p>
     */
    private final Map<Method, PropertyMetadata> m_writeMethodPropertyMetadata;

    /**
     * <p>
     * Construct a {@link PropertyAccessorCallback} instance.
     * </p>
     *
     * @param entityMetadata metadata for the entity class whose accessor invocations will be handled by this instance.
     */
    PropertyAccessorCallback(final EntityMetadata entityMetadata)
    {
        super();
        assert null != entityMetadata : "The [entityMetadata] argument cannot be null.";

        final Collection<PropertyMetadata> propertyMetadatas = entityMetadata.getProperties();
        m_entityMetadata = entityMetadata;
        m_readMethodPropertyMetadata = CollectionUtils.toMap(propertyMetadatas, READ_METHOD_EXTRACTOR);
        m_writeMethodPropertyMetadata = CollectionUtils.toMap(propertyMetadatas, WRITE_METHOD_EXTRACTOR);
    }

    /* inherit javadoc */
    public Object intercept(final Object instance,
                            final Method method,
                            final Object[] arguments,
                            final MethodProxy methodProxy)
            throws Throwable
    {
        // Determine whether the method is a getter or a setter.
        final boolean isReadMethod = m_readMethodPropertyMetadata.containsKey(method);
        final boolean isWriteMethod = !isReadMethod && m_writeMethodPropertyMetadata.containsKey(method);
        final Object result;
        if (!isReadMethod && !isWriteMethod)
        {
            throw new IllegalStateException(String.format("Callback [%s] invoked for unsupported method [%s].", this,
                    method));
        }
        else
        {
            // Invoke the internal handler for the instance.
            final ProxyInternal internal = (ProxyInternal)instance;
            if (isReadMethod)
            {
                // Method was a property getter.
                final PropertyMetadata propertyMetadata = m_readMethodPropertyMetadata.get(method);
                result = internal.handlePropertyRead(propertyMetadata);
            }
            else
            {
                // Method was a property setter.
                final PropertyMetadata propertyMetadata = m_writeMethodPropertyMetadata.get(method);
                internal.handlePropertyWrite(propertyMetadata, arguments[0]);

                // Support chained setters. TODO: Should this be configurable?
                final Class<?> returnType = method.getReturnType();
                if (m_entityMetadata.getEntityClass().equals(returnType))
                {
                    result = instance;
                }
                else
                {
                    result = null;
                }
            }
        }
        return result;
    }

    /**
     * <p>
     * {@link Extractor} instance which extracts the property read method from a {@link PropertyMetadata} instance.
     * </p>
     */
    private static final Extractor<Method, PropertyMetadata> READ_METHOD_EXTRACTOR =
            new Extractor<Method, PropertyMetadata>()
            {
                public Method extract(final PropertyMetadata object)
                {
                    return object.getReadMethod();
                }
            };

    /**
     * <p>
     * {@link Extractor} instance which extracts the property write method from a {@link PropertyMetadata} instance.
     * </p>
     */
    private static final Extractor<Method, PropertyMetadata> WRITE_METHOD_EXTRACTOR =
            new Extractor<Method, PropertyMetadata>()
            {
                public Method extract(final PropertyMetadata object)
                {
                    return object.getWriteMethod();
                }
            };
}
